﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace Niknak.Services.ServiceCore.TaskScheduler
{
    /// <summary>
    /// Provides the ability to perform a task according to a specified schedule
    /// </summary>
    /// <typeparam name="ConfigType">Class containing scheduling information</typeparam>
    public abstract class AdvancedTaskProcessor<ConfigType> where ConfigType : IAdvancedTaskConfig, new()
    {
        private Timer _Timer = null;
        private TimeSpan _StartTime;
        private TimeSpan _EndTime;
        private DateTime _CurrentExecutingIteration;
        /// <summary>
        /// Allows for maintaining a flag showing if the task is currently running.
        /// </summary>
        /// <remarks>
        /// This is set to true at the start of each iteration (e.g. top of ProcessTask), false at the end. It can be over-ridden and externally set for multi-threaded tasks.
        /// </remarks>
        protected bool _TaskInProgress = false;
        /// <summary>
        /// Allows for maintaining a flag showing if the processing engine is shutting down to avoid starting a new iteration
        /// </summary>
        protected /*static*/ bool _ShuttingDown = false;
        /// <summary>
        /// Delegate method required to run code on each iteration
        /// </summary>
        protected ProcessTask _TaskProcessor;

        private ConfigType _Settings;
        /// <summary>
        /// Object which contains implementation of IAdvancedTaskConfig
        /// </summary>
        public ConfigType Settings
        {
            get
            {
                if (_Settings == null)
                {
                    _Iterations = null;
                    _Settings = new ConfigType();
                }
                return _Settings;
            }
            set
            {
                _Iterations = null;
                _Settings = value;
            }
        }

        private DateTime[] _Iterations;
        /// <summary>
        /// Array of DateTime values on which the task is scheduled to run
        /// </summary>
        public DateTime[] Iterations
        {
            get
            {
                if (string.IsNullOrEmpty(Settings.EndDate))
                    throw new InvalidOperationException("Unable to calculate task iterations without an end date. End date is required, end time is optional.");

                if (_Iterations == null || !_Iterations.Any())
                {
                    List<DateTime> iterations = new List<DateTime>();
                    iterations.Add(CalculateTargetStart());

                    DateTime iterationPtr = iterations.First();
                    TimeSpan interval = TimeSpan.MaxValue;

                    switch (Settings.IntervalType)
                    {
                        case InternalEnums.TaskScheduler.IntervalType.Minutely:
                            interval = new TimeSpan(0, 1, 0);
                            break;
                        case InternalEnums.TaskScheduler.IntervalType.Hourly:
                            interval = new TimeSpan(1, 0, 0);
                            break;
                        case InternalEnums.TaskScheduler.IntervalType.Daily:
                            interval = new TimeSpan(1, 0, 0, 0);
                            break;
                        case InternalEnums.TaskScheduler.IntervalType.Weekly:
                            interval = new TimeSpan(7, 0, 0, 0);
                            break;
                        case InternalEnums.TaskScheduler.IntervalType.Fortnightly:
                            interval = new TimeSpan(14, 0, 0, 0);
                            break;
                        case InternalEnums.TaskScheduler.IntervalType.Custom:
                            string[] intervalParts = Settings.CustomInterval.Split(':');

                            if (!intervalParts.Length.Equals(3))
                                throw new Exception("Invalid scheduled task custom interval detected; use the format d.HH:mm:ss");

                            interval = TimeSpan.Parse(Settings.CustomInterval);

                            if (interval.TotalMilliseconds > UInt32.MaxValue)
                                throw new Exception("The interval for the task is too large and exceeds UInt32.MaxValue (i.e. ~49 days). Specified interval was ~" + ((int)interval.TotalDays).ToString() + " days.");
                            break;
                    }

                    DateTime calculatedTargetEnd = CalculateTargetEnd();

                    while (iterationPtr < calculatedTargetEnd)
                    {
                        if (Settings.IntervalType == InternalEnums.TaskScheduler.IntervalType.Monthly)
                            iterationPtr = iterationPtr.AddMonths(1);
                        else
                            iterationPtr += interval;

                        if (iterationPtr > calculatedTargetEnd)
                            continue;
                        if (!IsValidIterationDay(Settings, iterationPtr))
                            continue;

                        //if the end time is set we need to make sure this iteration is within the time boundaries - how did i miss this :/
                        if (!string.IsNullOrEmpty(Settings.EndTime) && Settings.StartTime != Settings.EndTime && (iterationPtr < iterationPtr.Date.Add(CalculateTargetStart().TimeOfDay) || iterationPtr > iterationPtr.Date.Add(CalculateTargetEnd().TimeOfDay)))
                            continue;

                        iterations.Add(iterationPtr);
                    }


                    _Iterations = iterations.ToArray();
                }

                return _Iterations;
            }
        }

        /// <summary>
        /// Uses the supplied config information to calculate the target start date for the task
        /// </summary>
        public DateTime CalculateTargetStart()
        {
            DateTime targetStart = DateTime.MinValue;

            //ideally the target start is a combination of the start date and the start time
            if (!DateTime.TryParseExact(Settings.StartDate + " " + Settings.StartTime, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.AssumeUniversal, out targetStart))
            {
                targetStart = DateTime.MinValue;

                if (!string.IsNullOrEmpty(Settings.StartDate))
                {
                    string[] startDateParts = Settings.StartDate.Split('-');
                    if (!startDateParts.Length.Equals(3))
                        throw new Exception("Invalid scheduled task start date detected; use the format yyyy-MM-dd");
                }

                string[] startTimeParts = Settings.StartTime.Split(':');
                if (!startTimeParts.Length.Equals(3))
                    throw new Exception("Invalid scheduled task start time detected; use the format d.HH:mm:ss");

                _StartTime = TimeSpan.Parse(Settings.StartTime);
                targetStart = DateTime.UtcNow.Date.Add(_StartTime);
            }
            else
                targetStart = targetStart.ToUniversalTime();

            if (!IsValidIterationDay(Settings, targetStart))
                throw new Exception("Target start date falls on a day on which the task is not allowed to run");

            return targetStart;
        }

        /// <summary>
        /// Uses the supplied config information to calculate the target end date for the task
        /// </summary>
        public DateTime CalculateTargetEnd()
        {
            DateTime targetEnd = DateTime.MaxValue;

            //new for advanced tasks is the concept of an end time and date
            if (!DateTime.TryParseExact(Settings.EndDate + " " + Settings.EndTime, "yyyy-MM-dd HH:mm:ss", null, System.Globalization.DateTimeStyles.AssumeUniversal, out targetEnd))
            {
                targetEnd = DateTime.MaxValue;

                if (!string.IsNullOrEmpty(Settings.EndDate))
                {
                    string[] endDateParts = Settings.EndDate.Split('-');
                    if (!endDateParts.Length.Equals(3))
                        throw new Exception("Invalid scheduled task end date detected; use the format yyyy-MM-dd");

                    targetEnd = DateTime.ParseExact(Settings.EndDate, "yyyy-MM-dd", null, System.Globalization.DateTimeStyles.AssumeUniversal);
                }

                if (!string.IsNullOrEmpty(Settings.EndTime))
                {
                    string[] endTimeParts = Settings.EndTime.Split(':');
                    if (!endTimeParts.Length.Equals(3))
                        throw new Exception("Invalid scheduled task end time detected; use the format d.HH:mm:ss");

                    _EndTime = TimeSpan.Parse(Settings.EndTime);
                    targetEnd = DateTime.UtcNow.Date.Add(_EndTime);
                }
                else
                {
                    _EndTime = TimeSpan.Parse(Settings.StartTime);
                    targetEnd = targetEnd.Add(_EndTime);
                }
            }
            else
                targetEnd = targetEnd.ToUniversalTime();

            if (!IsValidIterationDay(Settings, targetEnd))
                throw new Exception("Target end date falls on a day on which the task is not allowed to run");

            return targetEnd;
        }

        private static bool IsValidIterationDay(ConfigType config, DateTime forIterationDateTime)
        {
            if (config.RunOnDays != InternalEnums.TaskScheduler.DaysOfWeek.Unknown && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.All))
            {
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Monday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Monday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Tuesday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Tuesday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Wednesday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Wednesday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Thursday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Thursday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Friday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Friday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Saturday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Saturday))
                    return false;
                if (forIterationDateTime.DayOfWeek == DayOfWeek.Sunday && !config.RunOnDays.HasFlag(InternalEnums.TaskScheduler.DaysOfWeek.Sunday))
                    return false;
            }

            return true;
        }

        /// <summary>
        /// Converts a 'rough' iteration date (e.g. from DateTime.UtcNow) into an exact iteration date as calculated from the task using the original start date and interval
        /// </summary>
        public DateTime RoughIterationDateToExact(DateTime roughIterationDate)
        {
            //ditch the milliseconds
            DateTime sanitisedDate = new DateTime(roughIterationDate.Year, roughIterationDate.Month, roughIterationDate.Day, roughIterationDate.Hour, roughIterationDate.Minute, roughIterationDate.Second);

            //look up the exact iteration datetime but allow for pre-configured drift
            DateTime exactIteration = Iterations.Where(i => i == sanitisedDate).SingleOrDefault();

            //if there wasn't an exact match for this iteration date look for a match falling within +/- the allowable drift
            if (exactIteration == null || exactIteration.Equals(DateTime.MinValue))
                exactIteration = Iterations
                    .Where(i => sanitisedDate >= i.Subtract(TimeSpan.FromSeconds(Settings.AllowableDrift)) && sanitisedDate <= i.AddSeconds(Settings.AllowableDrift))
                    .SingleOrDefault();

            return exactIteration;
        }

        /// <summary>
        /// Starts the scheduler and runs ProcessTask at the configured interval.
        /// </summary>
        public virtual void Initialise()
        {
            if (_TaskProcessor == null)
                throw new Exception("Task processor delegate is undefined");

            _ShuttingDown = false;
            RestartTimer();
        }

        private void RestartTimer()
        {
            if (_Timer != null)
                _Timer.Dispose();

            if (_ShuttingDown)
                return;

            DateTime targetStart = CalculateTargetStart();
            DateTime targetEnd = CalculateTargetEnd();

            //make sure we're not about to start a task that should have finished already
            if (targetStart > targetEnd)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Task timer not started because the calculated start date falls after the calculated end date");
                return;
            }

            IEnumerable<DateTime> futureIterations = Iterations.OrderBy(i => i).Where(i => i > (_CurrentExecutingIteration == DateTime.MinValue ? DateTime.UtcNow : _CurrentExecutingIteration));
            if (futureIterations.Any())
            {
                targetStart = futureIterations.First();

                //calculate the time between now and the first iteration - this is how long to wait before the first timer callback
                TimeSpan initialDue = targetStart.Subtract(DateTime.UtcNow);

                if (initialDue.TotalMilliseconds > UInt32.MaxValue)
                    throw new Exception("The initial due time of the task is too far in the future and exceeds UInt32.MaxValue (i.e. ~49 days). Specified initial due time was ~" + ((int)initialDue.TotalDays).ToString() + " days.");
                
                //if we're a few ms behind for some reason, correct to start immediately
                if (initialDue.TotalMilliseconds < 0D)
                    initialDue = TimeSpan.FromMilliseconds(0D);

                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Information, "Next iteration scheduled to run in " + initialDue.ToString() + " at " + targetStart.ToString());
                TimeSpan nextInterval = Iterations.OrderBy(i => i).Where(i => i >= targetStart).First() - targetStart;

                //if the internal is out slightly fix it up to run immediately
                if (nextInterval.TotalMilliseconds < 0D)
                    nextInterval = TimeSpan.FromMilliseconds(0D);

                _Timer = new Timer(
                    new TimerCallback(ValidateAndRunIteration),
                    Settings,
                    initialDue,
                    nextInterval
                );
            }
        }

        /// <summary>
        /// Shuts down the task processor by setting _ShuttingDown to true and disposing the internal timer. The TaskShuttingDown event is also raised so tasks can be cleanly stopped.
        /// </summary>
        public void Shutdown()
        {
            _ShuttingDown = true;
            if (TaskShuttingDown != null)
                try
                {
                    TaskShuttingDown(this, EventArgs.Empty);
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in TaskShuttingDown event: " + ex.Message);
                }

            if (_Timer != null)
                _Timer.Dispose();
        }

        /// <summary>
        /// Called on every task iteration to check that this iteration falls within the acceptible time range to run
        /// </summary>
        /// <param name="obj">State object from the timer (implements IAdvancedTaskConfig)</param>
        protected void ValidateAndRunIteration(object obj)
        {
            ConfigType config = (ConfigType)obj;
            _CurrentExecutingIteration = RoughIterationDateToExact(DateTime.UtcNow);

            //first job is to restart the timer - necessary because monthly schedules move around depending on the length of the month, so a constant interval is not possible
            RestartTimer();

            if (_ShuttingDown)
                return;

            //is this task still running? if so get out now
            if (_TaskInProgress)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Unable to start task iteration because a previous task iteration is still running");
                return;
            }

            //not found in the list? bye bye
            if (_CurrentExecutingIteration == DateTime.MinValue)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Unable to start task iteration because the current DateTime of " + DateTime.UtcNow.ToString() + " was not found in the list of calculated iterations for this task");
                return;
            }

            //important thing to remember is we can't get here unless the timing information is good and the task is supposed to run right now

            Timer terminator = null;

            //if there is a time boundary and TerminateAtEndTime is set then we need to forcibly stop the task iteration at the EndTime, rather than letting it continue to run into the next iteration
            if (!string.IsNullOrEmpty(config.EndTime) && config.TerminateAtEndTime)
            {
                TimeSpan terminateAt = _CurrentExecutingIteration.Date.Add(TimeSpan.Parse(config.EndTime)) - _CurrentExecutingIteration;

                //this came up because invalid iterations were being generated when an EndTime different to the StartTime was set
                //an iteration would get scheduled when it wasn't supposed to be, and the terminateAt value would be in the past - not good
                //but just in case, if the terminate time is in the past we should not do this iteration (and if it was still running we'd have exited by now)
                if (terminateAt.TotalMilliseconds <= 0 || terminateAt.TotalMilliseconds > UInt32.MaxValue)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Warning, "Unable to configure terminator, termination TimeSpan out of range; terminateAt = " + terminateAt.ToString() + "; _CurrentExecutingIteration = " + _CurrentExecutingIteration.ToString() + "; config.EndTime = " + config.EndTime);
                    return;
                }
                else
                {
                    terminator = new Timer(
                        new TimerCallback(Terminator),
                        new IterationEventArgs(config, _CurrentExecutingIteration),
                        terminateAt,
                        TimeSpan.Zero
                    );

                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Information, "Terminator configured to end task in " + (_CurrentExecutingIteration.Date.Add(TimeSpan.Parse(config.EndTime)) - _CurrentExecutingIteration).ToString() + " at " + _CurrentExecutingIteration.Date.Add(TimeSpan.Parse(config.EndTime)).ToString());
                }
            }

            if (IterationStarting != null)
                try
                {
                    IterationStarting(this, new IterationEventArgs(config, _CurrentExecutingIteration));
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Iteration aborted. Exception raised in IterationStarting event: " + ex.Message);
                    _TaskInProgress = false;
                    _CurrentExecutingIteration = DateTime.MinValue;
                    return;
                }

            _TaskInProgress = true;
            try
            {
                _TaskProcessor(obj);
            }
            catch (Exception ex)
            {
                LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in ProcessTask delegate: " + ex.Message);
            }
            _TaskInProgress = false;

            if (terminator != null)
                terminator.Dispose();

            if (IterationCompleted != null)
                try
                {
                    IterationCompleted(this, new IterationEventArgs(config, _CurrentExecutingIteration));
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in IterationCompleted event: " + ex.Message);
                }

            _CurrentExecutingIteration = DateTime.MinValue;
            LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Information, "ValidateAndRunIteration has completed");
        }

        /// <summary>
        /// Allows for the completed event to be raised after the iteration runs. Useful when the completed event has been disabled or failed.
        /// </summary>
        /// <param name="iterationDate">Date of the iteration for context</param>
        public void RaiseIterationCompletedEvent(DateTime iterationDate)
        {
            if (IterationCompleted != null)
                try
                {
                    IterationEventArgs args = new IterationEventArgs(Settings, iterationDate);
                    args.EnableOverrideFlag();

                    IterationCompleted(this, args);
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in IterationCompleted event: " + ex.Message);
                }
        }

        /// <summary>
        /// Raised before the task iteration begins
        /// </summary>
        public event EventHandler<IterationEventArgs> IterationStarting;

        /// <summary>
        /// Raised when the task iteration should be terminated to prevent over-running
        /// </summary>
        public event EventHandler<IterationEventArgs> IterationTerminationRequest;

        private void Terminator(object taskEventArgs)
        {
            if (IterationTerminationRequest != null)
                try
                {
                    IterationTerminationRequest(this, (IterationEventArgs)taskEventArgs);
                }
                catch (Exception ex)
                {
                    LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity.Error, "Exception raised in IterationTerminationRequest event: " + ex.Message);
                    return;
                }
        }

        /// <summary>
        /// Raised when the task iteration ends
        /// </summary>
        public event EventHandler<IterationEventArgs> IterationCompleted;

        /// <summary>
        /// Raised when the task is beginning its shutdown routine
        /// </summary>
        public event EventHandler TaskShuttingDown;

        /// <summary>
        /// Raised when a logging message is generated
        /// </summary>
        public event EventHandler<LoggingEventArgs> LogMessageGenerated;

        private void LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity severity, string message)
        {
            LogMessage(severity, message, null);
        }

        private void LogMessage(InternalEnums.TaskScheduler.LoggingMessageSeverity severity, string message, Exception exception)
        {
            if (LogMessageGenerated != null)
                try
                {
                    LogMessageGenerated(this, new LoggingEventArgs(Settings, _CurrentExecutingIteration, severity, message, exception));
                }
                catch { }
        }

        /// <summary>
        /// Method to call on each timer iteration
        /// </summary>
        /// <param name="obj">State object which is a copy of the Settings property</param>
        protected delegate void ProcessTask(object obj);
    }
}
